昨天我們已經將 LeetCode 題目呈現在 SwiftUI 的 List 上了,而我們今天要挑戰的是,資料取得改從 Network API 請求去拿資料,並且成功顯示在頁面上,所以主要是網路連線,JSON 資料轉換成物件,最後塞入 UI 元件的過程
API 全名是 Application Programming Interface,翻譯成中文是應用程式介面,聽起來好像是中文的手機裝置畫面,但並不是,它的概念比較通用廣泛,它是提供一個溝通媒介,讓兩端溝通時用統一的格式語言,而兩邊的程式不需要知道對方實作細節,只需要拿到他想要的資料即可,不過這是我的理解後所定義,更專業的定義可以去維基百科查詢
LeetCode 其實沒有一個正式的 API 文件可以讓開發者去使用,但是卻有 API 可以獲得所有題目的資料,需要透過網路取得的 API 我們稱為 Web API,如果有符合 Restful API 風格,則稱為 Restful API,如下網址
https://leetcode.com/api/problems/algorithms/
實際上拿到密密麻麻亂七八糟的文字,我們將這些文字貼到 http://json.parser.online.fr/ 這個網站,它會幫我們整理出比較漂亮的且有縮行的 JSON 格式,方便我們檢視每個資料代表的意義,並轉換成 Swift 物件
接下來就是要把 JSON 資料轉換成 Swift 物件了!而這個 API 複雜度實在非常之高,身為工程師實在很懶得一個一個拼揍,於是藉由工具的力量產生,網站在此:https://quicktype.io/
讓我們把這個密密麻麻 LeetCode API JSON 資料輕鬆轉換成 Swift 物件,程式碼如下
// MARK: - Welcome
struct Welcome: Codable, Identifiable {
let id = UUID()
let userName: String
let numSolved, numTotal, acEasy, acMedium: Int
let acHard: Int
let statStatusPairs: [StatStatusPair]
let frequencyHigh, frequencyMid: Int
let categorySlug: String
enum CodingKeys: String, CodingKey {
case userName = "user_name"
case numSolved = "num_solved"
case numTotal = "num_total"
case acEasy = "ac_easy"
case acMedium = "ac_medium"
case acHard = "ac_hard"
case statStatusPairs = "stat_status_pairs"
case frequencyHigh = "frequency_high"
case frequencyMid = "frequency_mid"
case categorySlug = "category_slug"
}
}
// MARK: - StatStatusPair
struct StatStatusPair: Codable, Identifiable {
let id = UUID()
let stat: Stat
let status: Status?
let difficulty: Difficulty
let paidOnly, isFavor: Bool
let frequency, progress: Int
enum CodingKeys: String, CodingKey {
case stat, status, difficulty
case paidOnly = "paid_only"
case isFavor = "is_favor"
case frequency, progress
}
}
// MARK: - Difficulty
struct Difficulty: Codable {
let level: Int
}
// MARK: - Stat
struct Stat: Codable {
let questionID: Int
let questionArticleLive: Bool?
let questionArticleSlug: String?
let questionArticleHasVideoSolution: Bool?
let questionTitle, questionTitleSlug: String
let questionHide: Bool
let totalAcs, totalSubmitted, frontendQuestionID: Int
let isNewQuestion: Bool
enum CodingKeys: String, CodingKey {
case questionID = "question_id"
case questionArticleLive = "question__article__live"
case questionArticleSlug = "question__article__slug"
case questionArticleHasVideoSolution = "question__article__has_video_solution"
case questionTitle = "question__title"
case questionTitleSlug = "question__title_slug"
case questionHide = "question__hide"
case totalAcs = "total_acs"
case totalSubmitted = "total_submitted"
case frontendQuestionID = "frontend_question_id"
case isNewQuestion = "is_new_question"
}
}
enum Status: String, Codable {
case ac = "ac"
case notac = "notac"
}
但是我們的 SwiftUI 列表 List 不需要這麼過度複雜的資料,只需要拿取部分的標題跟 LeetCode 題目等級的資料,所以我們製作了一個專為 UI 資料顯示的資料,程式碼如下
struct LeetCodeProblem: Identifiable {
var id: UUID?
var title: String?
var description: String?
}
而這兩種資料格式你都會發現後面多了一個 Identifiable
因為 SwiftUI 的列表需要識別獨立的 ID,好讓它能夠認得列表中每個不同的資料元素,我們也多設定了一個 var id: UUID?
這是原本 JSON 資料沒有的,由我們自己程式自己產獨立的 UUID 識別
利用 Swift 的 URLSession 工具去取得 API 請求資料,如下程式碼,因為網路有可能失敗或是JSON 解析拿不到資料,所以有返回 nil 的可能性,且我們將從 Web API 的請求資料用 JSONDecoder
去轉換成 Welcome
物件,成功轉換後,我們在將 UI 要顯示的資料對應到 LeetCodeProblem
物件,並且組成 var problems: [LeetCodeProblem]
這個陣列返回給 SwiftUI 的 List
func fetchProblems(completion: @escaping ([LeetCodeProblem]?) -> Void) {
guard let url = URL(string: "https://leetcode.com/api/problems/all/") else {
completion(nil)
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
completion(nil)
return
}
do {
let leetCodeData = try JSONDecoder().decode(Welcome.self, from: data)
var problems: [LeetCodeProblem] = []
leetCodeData.statStatusPairs.forEach { statStatusPair in
if(!statStatusPair.paidOnly) {
var p = LeetCodeProblem()
p.id = statStatusPair.id
p.title = statStatusPair.stat.questionTitle
switch(statStatusPair.difficulty.level) {
case 1:
p.description = "Level: Easy"
case 2:
p.description = "Level: Medium"
case 3:
p.description = "Level: Hard"
default:
p.description = ""
}
problems.append(p)
}
}
completion(problems)
} catch {
print(error)
completion(nil)
}
}.resume()
}
順帶一提,我們只顯示不用付費的 LeetCode 題目哦!所以有用 !statStatusPair.paidOnly
去過濾判斷非付費題目才會顯示於列表上
我們利用 .onAppear
的閉包去取得網路請求 API 資料,並且通知屬於 @State
的 problems
陣列,讓他去更新 UI 顯示相關的 LeetCode 題目資料
struct ContentView: View {
@State private var problems: [LeetCodeProblem] = []
var body: some View {
List(problems) { problem in
VStack(alignment: .leading) {
if let title = problem.title {
Text(title)
.font(.headline)
}
Text(problem.description ?? "")
.font(.subheadline)
}
}
.onAppear {
fetchProblems { fetchedProblems in
if let fetchedProblems = fetchedProblems {
self.problems = fetchedProblems
}
}
}
}
}
最終我們的畫面成功依照 LeetCode API 顯示 LeetCode 題目囉!顯示的當下十分有成就感,邁進了一大步的感覺
在撰寫 SwiftUI 的過程中,本來想做防呆判斷所以原本的寫法是如下,但是 Text
吃的字串不可以是 nil,即使你判斷了它不是 nil
VStack(alignment: .leading) {
// NG 寫法
if(problem.title != nil) {
Text(problem.title)
.font(.headline)
}
}
這個寫法是拆包確定不是 nil 後,塞值到 title,如果是 nil 這個 Text
就不會生成出現
VStack(alignment: .leading) {
// OK 寫法
if let title = problem.title {
Text(title)
.font(.headline)
}
}
而本文範例寫法是即使是 nil 仍給他一個空字串生成 Text
字串,這沒有正確答案,只有看需求顯示當下最合宜的防呆
Text(problem.title ?? "")
.font(.headline)
本日成功的讓我們的資料不在寫死在 App 端,而是透過網路請求獲得 LeetCode 題目資料,這代表只要 LeetCode 題目增加,我們的 App 可以隨之即時更新,不需要再透過 App 程式碼的更改,又要再重新上架 App 過送審的動作,讓 App 更靈活呈現在使用者的畫面上!